package yatan.common.proxypool;

import java.io.IOException;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

public class DelayProxyPool {
    private static DelayProxyPool instance;

    private final Logger logger = Logger.getLogger(getClass());

    private final List<ProxyRecord> proxyRecords = new ArrayList<ProxyRecord>();

    private long delayUnit = 2 * 1000;
    private long failDelayUnit = 64 * 1000;
    private long maxUseCountLimit = 2;

    public synchronized static DelayProxyPool getInstance() {
        if (instance == null) {
            instance = new DelayProxyPool();
        }

        return instance;
    }

    private DelayProxyPool() throws ProxyPoolConfigurationException {
        loadProxies("proxy2.txt", "proxy3.txt", "auto-proxy.txt");
        this.logger.info("Proxy pool initialized successfully. Loaded " + this.proxyRecords.size() + " proxies.");
    }

    public synchronized void loadProxies(String... filePaths) throws ProxyPoolConfigurationException {
        checkNull(filePaths, "filePath");

        StringBuilder sb = new StringBuilder();
        for (String filePath : filePaths) {
            InputStream is = getClass().getClassLoader().getResourceAsStream(filePath);
            InputStreamReader reader = null;
            try {
                reader = new InputStreamReader(is);
                char[] buffer = new char[4096];
                int len = reader.read(buffer);
                while (len > -1) {
                    sb.append(buffer, 0, len);
                    len = reader.read(buffer);
                }
            } catch (IOException e) {
                throw new ProxyPoolConfigurationException("Cannot read proxy file " + filePath, e);
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
        }

        Set<String> hosts = new HashSet<String>();
        Random random = new Random(new Date().getTime());
        Pattern pattern = Pattern.compile("(\\d+\\.\\d+\\.\\d+\\.\\d+):(\\d+)");
        for (String line : sb.toString().split("\n")) {
            Matcher matcher = pattern.matcher(line);
            if (matcher.find()) {
                Proxy proxy = new Proxy();
                proxy.setHost(matcher.group(1));
                proxy.setPort(Integer.parseInt(matcher.group(2)));

                if (!hosts.contains(proxy.getHost())) {
                    hosts.add(proxy.getHost());

                    if (this.proxyRecords.isEmpty()) {
                        this.proxyRecords.add(new ProxyRecord(proxy));
                    } else {
                        this.proxyRecords.add(random.nextInt(this.proxyRecords.size()), new ProxyRecord(proxy));
                    }
                }
            }
        }
    }

    private long totalUseCount;

    private void audit(int availableSize) {
        if (this.totalUseCount++ % 1000 == 0) {
            long maxUseCount = 0;
            long minUseCount = Long.MAX_VALUE;
            long maxDelay = 0;
            long minDelay = Long.MAX_VALUE;
            long good = 0;
            int totalUse = 0;
            for (ProxyRecord proxyRecord : this.proxyRecords) {
                if (proxyRecord.getTotalUseCount() > maxUseCount) {
                    maxUseCount = proxyRecord.getTotalUseCount();
                }
                if (proxyRecord.getTotalUseCount() < minUseCount) {
                    minUseCount = proxyRecord.getTotalUseCount();
                }
                if (proxyRecord.getDelay() > maxDelay) {
                    maxDelay = proxyRecord.getDelay();
                }
                if (proxyRecord.getDelay() < minDelay) {
                    minDelay = proxyRecord.getDelay();
                }
                if (proxyRecord.getDelay() == this.delayUnit) {
                    good++;
                }
                totalUse += proxyRecord.getUseCount();
            }

            logger.info("Proxy pool statistics: max = " + maxUseCount + ", min = " + minUseCount + ", total = "
                    + totalUseCount + ", min delay = " + minDelay + ", max delay = " + maxDelay + ", available = "
                    + availableSize + ", good = " + good + ", total in use = " + totalUse);
        }
    }

    public synchronized Proxy nextProxy() {
        List<ProxyRecord> available = new ArrayList<ProxyRecord>();
        while (available.isEmpty()) {
            for (ProxyRecord proxyRecord : this.proxyRecords) {
                if (proxyRecord.isAvailable()) {
                    available.add(proxyRecord);
                }
            }

            if (available.isEmpty()) {
                this.logger.debug("No proxy available. Wait for a proxy to be released.");
                try {
                    wait(1000);
                } catch (InterruptedException e) {
                    this.logger.warn(e.getMessage(), e);
                    // ignore
                }
            }
        }

        audit(available.size());

        Collections.sort(available);

        ProxyRecord proxyRecord = available.get(0);
        proxyRecord.use();

        return proxyRecord.getProxy();
    }

    public synchronized void reportProxyFail(Proxy proxy) {
        checkNull(proxy, "proxy");

        for (ProxyRecord proxyRecord : this.proxyRecords) {
            if (proxyRecord.getProxy().getHost().equals(proxy.getHost())) {
                proxyRecord.report(false);
                break;
            }
        }
    }

    public synchronized void reportProxyOK(Proxy proxy) {
        checkNull(proxy, "proxy");

        for (ProxyRecord proxyRecord : this.proxyRecords) {
            if (proxyRecord.getProxy().getHost().equals(proxy.getHost())) {
                proxyRecord.report(true);
                break;
            }
        }
    }

    private class ProxyRecord implements Comparable<ProxyRecord> {
        private Proxy proxy;
        private long lastReportTime = new Date().getTime();
        private long delay = DelayProxyPool.this.delayUnit;
        private int useCount;
        private int totalUseCount;

        public ProxyRecord(Proxy proxy) {
            this.proxy = proxy;
        }

        /**
         * <p>
         * Getter method for the proxy, simply return the value of the namesake field.
         * </p>
         * @return the proxy
         */
        public Proxy getProxy() {
            return proxy;
        }

        public long getLastReportTime() {
            return lastReportTime;
        }

        public long getDelay() {
            return delay;
        }

        public int getTotalUseCount() {
            return this.totalUseCount;
        }

        public int getUseCount() {
            return this.useCount;
        }

        public boolean isAvailable() {
            return this.useCount < DelayProxyPool.this.maxUseCountLimit
                    && (new Date().getTime() - this.lastReportTime - (this.delay * (this.useCount + 1)) >= 0);
        }

        public void use() {
            this.totalUseCount++;
            this.useCount++;
        }

        public void report(boolean success) {
            this.lastReportTime = new Date().getTime();

            if (success) {
                this.delay = DelayProxyPool.this.delayUnit;
            } else {
                if (this.delay < Long.MAX_VALUE / 2) {
                    if (this.delay < DelayProxyPool.this.failDelayUnit) {
                        this.delay = DelayProxyPool.this.failDelayUnit;
                    } else {
                        this.delay = this.delay * 2;
                    }
                } else {
                    this.delay = Long.MAX_VALUE;
                }
            }

            this.useCount--;
        }

        @Override
        public int compareTo(ProxyRecord o) {
            long value =
                    (this.lastReportTime + this.delay * (this.useCount + 1))
                            - (o.lastReportTime + o.delay * (o.useCount + 1));
            if (value > Integer.MAX_VALUE) {
                return Integer.MAX_VALUE;
            }

            return (int) value;
        }
    }

    private static void checkNull(Object obj, String name) {
        if (obj == null) {
            throw new IllegalArgumentException("The argument '" + name + "' cannot be null.");
        }
    }

    private static void checkStringNullEmpty(String str, String name) {
        if (str == null || str.trim().length() == 0) {
            throw new IllegalArgumentException("The string argument '" + name + "' cannot be null or empty.");
        }
    }
}
